GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Push — master ( 7c1760...1d0edd )
by Richard
03:15
created

ticketer.resetEditTicketBlock   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
/**
2
 * Created by wechsler on 04/08/15.
3
 */
4
var ticketer = (function() {
5
  'use strict';
6
7
  return {
8
    upcomingTicketTemplate: null,
9
    manageTemplate: null,
10
    songAutocompleteItemTemplate: null,
11
    editTicketTemplate: null,
12
    songDetailsTemplate: null,
13
    manageInstrumentTabsTemplate: null,
14
    appMessageTarget: null,
15
    searchCount: 10,
16
    instrumentOrder: null,
17
    defaultSongLengthSeconds: 240,
18
    defaultSongIntervalSeconds: 120,
19
    messageTimer: null,
20
21
    /**
22
     * @var {{songInPreview,upcomingCount,iconMapHtml}}
23
     */
24
    displayOptions: {},
25
26
    /**
27
     * List of all performers (objects) who've signed up in this session
28
     */
29
    performers: [],
30
31
    /**
32
     * List of all platform names in the system
33
     */
34
    platforms: [],
35
36
    performerExists: function(performerName) {
37
      for (var i = 0; i < this.performers.length; i++) {
38
        if (this.performers[i].performerName.toLowerCase() == performerName.toLowerCase()) {
39
          return true;
40
        }
41
      }
42
      return false;
43
    },
44
45
    addPerformerByName: function(performerName) {
46
      this.performers.push({performerName: performerName});
47
      // Now resort it
48
      this.performers.sort(function(a, b) {
49
        return a.performerName.localeCompare(b.performerName);
50
      });
51
    },
52
53
    /**
54
     * Run the "upcoming" panel
55
     */
56
    go: function() {
57
      this.initTemplates();
58
59
      ticketer.reloadTickets();
60
      setInterval(function() {
61
        ticketer.reloadTickets();
62
      }, 10000);
63
    },
64
65
    /**
66
     * Draw an "upcoming" ticket
67
     * @param ticket {{band}}
68
     * @returns {*}
69
     */
70
    drawDisplayTicket: function(ticket) {
71
      // Sort band into standard order
72
      var unsortedBand = ticket.band;
73
      var sortedBand = {};
74
      for (var i = 0; i < this.instrumentOrder.length; i++) {
75
        var instrument = this.instrumentOrder[i];
76
        if (unsortedBand.hasOwnProperty(instrument)) {
77
          sortedBand[instrument] = unsortedBand[instrument];
78
        }
79
      }
80
      ticket.band = sortedBand;
81
      var ticketParams = {ticket: ticket, icons: this.displayOptions.iconMapHtml};
82
      return this.upcomingTicketTemplate(ticketParams);
83
    },
84
85
    /**
86
     * Draw a "queue management" ticket
87
     * @param ticket
88
     * @returns {*}
89
     */
90
    drawManageableTicket: function(ticket) {
91
      ticket.used = Number(ticket.used); // Force int
92
93
      return this.manageTemplate({ticket: ticket});
94
    },
95
96
    /**
97
     * Reload all tickets on the upcoming page
98
     */
99
    reloadTickets: function() {
100
      var that = this;
101
102
      $.get('/api/next', function(tickets) {
103
104
        var out = '';
105
        for (var i = 0; i < tickets.length; i++) {
106
          var ticket = tickets[i];
107
          out += that.drawDisplayTicket(ticket);
108
        }
109
110
        var target = $('#target');
111
        target.html(out);
112
113
        target.find('.auto-font').each(
114
          function() {
115
            var fixedWidth = $(this).data('fixed-assetwidth');
116
            if (!fixedWidth) {
117
              fixedWidth = 0;
118
            }
119
            fixedWidth = Number(fixedWidth);
120
121
            var spaceUsedByText = (this.scrollWidth - fixedWidth);
122
            var spaceAvailableForText = (this.clientWidth - fixedWidth);
123
            var rawScale = Math.max(spaceUsedByText / spaceAvailableForText, 1);
124
            var scale = 1.05 * rawScale;
125
126
            if (that.displayOptions.adminQueueHasControls && that.displayOptions.isAdmin) {
127
              scale *= 1.25;
128
            }
129
130
            // 1.05 extra scale to fit neatly, fixedWidth is non-scaling elements
131
            var font = Number($(this).css('font-size').replace(/[^0-9]+$/, ''));
132
            $(this).css('font-size', Number(font / scale).toFixed() + 'px');
133
          }
134
        );
135
136
        target.find('.performingButton').click(function() {
137
          var ticketId = $(this).data('ticket-id');
0 ignored issues
show
Unused Code introduced by
The variable ticketId seems to be never used. Consider removing it.
Loading history...
138
          if (window.confirm('Mark song as performing?')) {
139
            that.performButtonCallback(this);
140
          }
141
        });
142
        target.find('.removeButton').click(function() {
143
          var ticketId = $(this).data('ticket-id');
0 ignored issues
show
Unused Code introduced by
The variable ticketId seems to be never used. Consider removing it.
Loading history...
144
          if (window.confirm('Remove song?')) {
145
            that.removeButtonCallback(this);
146
          }
147
        });
148
149
      });
150
    },
151
152
    /**
153
     * Enable queue management ticket buttons in the specified element
154
     * @param topElement
155
     */
156
    enableButtons: function(topElement) {
157
      var that = this;
158
159
      $(topElement).find('.performButton').click(function() {
160
        that.performButtonCallback(this);
161
      });
162
163
      $(topElement).find('.removeButton').click(function() {
164
        that.removeButtonCallback(this);
165
      });
166
167
      $(topElement).find('.editButton').click(function() {
168
        that.editButtonCallback(this);
169
      });
170
    },
171
172
    /**
173
     * Activate the search box at the given location
174
     *
175
     * @param {string} songSearchInput Input field identifier
176
     * @param {string} songSearchResultsTarget Container for output list
177
     * @param {function} songClickHandler Function to call when a listed song is clicked
178
     */
179
    enableSongSearchBox: function(songSearchInput, songSearchResultsTarget, songClickHandler) {
180
      var that = this;
181
      $(songSearchInput).keyup(
182
        function() {
183
          var songComplete = $(songSearchResultsTarget);
184
          var input = $(this);
185
          var searchString = input.val();
186
          if (searchString.length >= 3) {
187
            $.ajax({
188
              method: 'POST',
189
              data: {
190
                searchString: searchString,
191
                searchCount: that.searchCount
192
              },
193
              url: '/api/songSearch',
194
              /**
195
               * @param {{songs, searchString}} data
196
               */
197
              success: function(data) {
198
                var songs = data.songs;
199
                if (input.val() == data.searchString) {
200
                  // Ensure autocomplete response is still valid for current input value
201
                  var out = '';
202
                  var song; // Used twice below
203
                  for (var i = 0; i < songs.length; i++) {
204
                    song = songs[i];
205
                    out += that.songAutocompleteItemTemplate({song: song});
206
                  }
207
                  songComplete.html(out).show();
208
209
                  // Now attach whole song as data:
210
                  for (i = 0; i < songs.length; i++) {
211
                    song = songs[i];
212
                    var songId = song.id;
213
                    songComplete.find('.acSong[data-song-id=' + songId + ']').data('song', song);
214
                  }
215
216
                  that.enableAcSongSelector(songComplete, songClickHandler);
217
                }
218
              },
219
              error: function(xhr, status, error) {
220
                void(error);
221
              }
222
            });
223
          } else {
224
            songComplete.html('');
225
          }
226
        }
227
      );
228
    },
229
230
    /**
231
     * Completely (re)generate the add ticket control panel and enable its controls
232
     * @param {?number} currentTicket Optional
233
     */
234
    resetEditTicketBlock: function(currentTicket) {
235
      var that = this;
236
      var controlPanelOuter = $('.editTicketOuter');
237
238
      // Current panel state in function scope
239
      var selectedInstrument = 'V';
240
      var currentBand = {};
241
242
      // Reset band to empty (or to ticket band state)
243
      for (var instrumentIdx = 0; instrumentIdx < that.instrumentOrder.length; instrumentIdx++) {
244
        var instrument = that.instrumentOrder[instrumentIdx];
245
        currentBand[instrument] = [];
246
247
        if (currentTicket && currentTicket.band) {
248
          // Ticket.band is a complex datatype. Current band is just one array of names per instrument. Unpack to show.
249
          if (currentTicket.band.hasOwnProperty(instrument)) {
250
            var instrumentPerformerObjects = currentTicket.band[instrument];
251
            for (var pIdx = 0; pIdx < instrumentPerformerObjects.length; pIdx++) {
252
              currentBand[instrument].push(instrumentPerformerObjects[pIdx].performerName);
253
            }
254
          }
255
        }
256
        // Store all instruments as arrays - most can only be single, but vocals is 1..n potentially
257
      }
258
259
      drawEditTicketForm(currentTicket);
260
      // X var editTicketBlock = $('.editTicket'); // only used in inner scope (applyNewSong)
261
262
      // Enable 'Add' button
263
      $('.editTicketButton').click(editTicketCallback);
264
      $('.cancelTicketButton').click(cancelTicketCallback);
265
      $('.removeSongButton').click(removeSong);
266
267
      $('.toggleButton').click(
268
        function() {
269
          var check = $(this).find('input[type=checkbox]');
270
          check.prop('checked', !check.prop('checked'));
271
        }
272
      );
273
274
      var ticketTitleInput = $('.editTicketTitle');
275
276
      // Copy band name into summary area on Enter
277
      ticketTitleInput.keydown(function(e) {
278
        if (e.keyCode == 13) {
279
          updateBandSummary();
280
        }
281
      });
282
283
      $('.newPerformer').keydown(function(e) {
284
        if (e.keyCode == 13) {
285
          var newPerformerInput = $('.newPerformer');
286
          var newName = newPerformerInput.val();
287
          if (newName.trim().length) {
288
            alterInstrumentPerformerList(selectedInstrument, newName, true);
289
          }
290
          newPerformerInput.val('');
291
        }
292
      });
293
294
      // Set up the song search box in this control panel and set the appropriate callback
295
      var songSearchInput = '.addSongTitle';
296
      var songSearchResultsTarget = '.songComplete';
297
298
      this.enableSongSearchBox(songSearchInput, songSearchResultsTarget, applyNewSong);
299
300
      // ************* Inner functions **************
301
      /**
302
       * Switch to the next visible instrument tab
303
       */
304
      function nextInstrumentTab() {
305
        // Find what offset we're at in instrumentOrder
306
        var currentOffset = 0;
307
        for (var i = 0; i < that.instrumentOrder.length; i++) {
308
          if (that.instrumentOrder[i] == selectedInstrument) {
309
            currentOffset = i;
310
          }
311
        }
312
        var nextOffset = currentOffset + 1;
313
        if (nextOffset >= that.instrumentOrder.length) {
314
          nextOffset = 0;
315
        }
316
        var instrument = that.instrumentOrder[nextOffset];
317
        selectedInstrument = instrument; // Reset before we redraw tabs
318
        var newActiveTab = setActiveTab(instrument);
319
320
        // Make sure we switch to a *visible* tab
321
        if (newActiveTab.hasClass('instrumentUnused')) {
322
          nextInstrumentTab();
323
        }
324
      }
325
326
      /**
327
       * (re)Draw the add/edit ticket control panel in the .editTicketOuter element
328
       */
329
      function drawEditTicketForm(ticket) {
330
        var templateParams = {performers: that.performers};
331
        if (ticket) {
332
          templateParams.ticket = ticket;
333
        }
334
        controlPanelOuter.html(that.editTicketTemplate(templateParams));
335
        if (ticket && ticket.song) {
336
          applyNewSong(ticket.song);
337
        }
338
        updateInstrumentTabPerformers();
339
        rebuildPerformerList();
340
      }
341
342
      /**
343
       * Return the instrument abbreviation played by a given performer name
344
       *
345
       * @param name
346
       * @returns {*}
347
       */
348
      function findPerformerInstrument(name) {
349
        var instrumentPlayers;
350
        for (var instrumentCode in currentBand) {
351
          if (currentBand.hasOwnProperty(instrumentCode)) {
352
            instrumentPlayers = currentBand[instrumentCode];
353
            for (var i = 0; i < instrumentPlayers.length; i++) {
354
              if (instrumentPlayers[i].toUpperCase() == name.toUpperCase()) {
355
                return instrumentCode;
356
              }
357
            }
358
          }
359
        }
360
        return null;
361
      }
362
363
      /**
364
       * Rebuild list of performer buttons according to overall performers list
365
       * and which instruments they are assigned to
366
       */
367
      function rebuildPerformerList() {
368
        var newButton;
369
        var targetElement = controlPanelOuter.find('.performers');
370
        targetElement.text(''); // Remove existing list
371
372
        var lastInitial = '';
373
        var performerCount = that.performers.length;
374
        var letterSpan;
375
        for (var pIdx = 0; pIdx < performerCount; pIdx++) {
376
          var performerName = that.performers[pIdx].performerName;
377
          var performerInstrument = findPerformerInstrument(performerName);
378
          var isPerforming = performerInstrument ? 1 : 0;
379
          var initialLetter = performerName.charAt(0).toUpperCase();
380
          if (lastInitial !== initialLetter) {
381
            if (letterSpan) {
382
              targetElement.append(letterSpan);
383
            }
384
            letterSpan = $('<span class="letterSpan"></span>');
385
            if ((performerCount > 15)) {
386
              letterSpan.append($('<span class="initialLetter">' + initialLetter + '</span>'));
387
            }
388
          }
389
          lastInitial = initialLetter;
390
391
          newButton = $('<span></span>');
392
          newButton.addClass('btn addPerformerButton');
393
          newButton.addClass(isPerforming ? 'btn-primary' : 'btn-default');
394
          if (isPerforming && (performerInstrument !== selectedInstrument)) { // Dim out buttons for other instruments
395
            newButton.attr('disabled', 'disabled');
396
          }
397
          newButton.text(performerName);
398
          newButton.data('selected', isPerforming); // This is where it gets fun - check if user is in band!
399
          letterSpan.append(newButton);
0 ignored issues
show
Bug introduced by
The variable letterSpan seems to not be initialized for all possible execution paths.
Loading history...
400
        }
401
        targetElement.append(letterSpan);
402
403
        // Enable the new buttons
404
        $('.addPerformerButton').click(function() {
405
          var name = $(this).text();
406
          var selected = $(this).data('selected') ? 0 : 1; // Reverse to get new state
407
          if (selected) {
408
            $(this).removeClass('btn-default').addClass('btn-primary');
409
          } else {
410
            $(this).removeClass('btn-primary').addClass('btn-default');
411
          }
412
          $(this).data('selected', selected); // Toggle
413
414
          alterInstrumentPerformerList(selectedInstrument, name, selected);
415
        });
416
      }
417
418
      /**
419
       * Handle click on edit ticket button
420
       */
421
      function editTicketCallback() {
422
        var titleInput = $('.editTicketTitle');
423
        var ticketTitle = titleInput.val();
424
        var songInput = $('.selectedSongId');
425
        var songId = songInput.val();
426
        var privateCheckbox = $('input.privateCheckbox');
427
        var isPrivate = privateCheckbox.is(':checked');
428
        var blockingCheckbox = $('input.blockingCheckbox');
429
        var isBlocked = blockingCheckbox.is(':checked');
430
431
        var data = {
432
          title: ticketTitle,
433
          songId: songId,
434
          band: currentBand,
435
          private: isPrivate,
436
          blocking: isBlocked
437
        };
438
439
        if (currentTicket) {
440
          data.existingTicketId = currentTicket.id;
441
        }
442
443
        that.showAppMessage('Saving ticket');
444
445
        $.ajax({
446
            method: 'POST',
447
            data: data,
448
            url: '/api/saveTicket',
449
            success: function(data, status) {
450
              that.showAppMessage('Saved ticket', 'success');
451
452
              void(status);
453
              var ticketId = data.ticket.id;
454
455
              var ticketBlockSelector = '.ticket[data-ticket-id="' + ticketId + '"]';
456
              var existingTicketBlock = $(ticketBlockSelector);
457
              if (existingTicketBlock.length) {
458
                // Replace existing
459
                existingTicketBlock.after(that.drawManageableTicket(data.ticket));
460
                existingTicketBlock.remove();
461
              } else {
462
                // Append new
463
                $('#target').append(that.drawManageableTicket(data.ticket));
464
              }
465
466
              var ticketBlock = $(ticketBlockSelector);
467
              ticketBlock.data('ticket', data.ticket);
468
              that.enableButtons(ticketBlock);
469
470
              if (data.performers) {
471
                that.performers = data.performers;
472
              }
473
474
              that.updatePerformanceStats();
475
              that.resetEditTicketBlock();
476
477
            },
478
            error: function(xhr, status, error) {
479
              var message = 'Ticket save failed';
480
              that.reportAjaxError(message, xhr, status, error);
481
              void(error);
482
              // FIXME handle error
483
            }
484
          }
485
        );
486
      }
487
488
      function cancelTicketCallback() {
489
        that.resetEditTicketBlock();
490
      }
491
492
      /**
493
       * Return tab corresponding to a given instrument abbreviation
494
       *
495
       * @param {string} instrument Abbreviation
496
       * @returns {jQuery}
497
       */
498
      function getTabByInstrument(instrument) {
499
        return controlPanelOuter.find('.instrument[data-instrument-shortcode=' + instrument + ']');
500
      }
501
502
      /**
503
       * Set the tab for the specified instrument abbreviation as active
504
       *
505
       * @param selectedInstrument
506
       * @returns {jQuery}
507
       */
508
      function setActiveTab(selectedInstrument) {
509
        var allInstrumentTabs = controlPanelOuter.find('.instrument');
510
        allInstrumentTabs.removeClass('instrumentSelected');
511
        var selectedTab = getTabByInstrument(selectedInstrument);
512
        selectedTab.addClass('instrumentSelected');
513
        rebuildPerformerList(); // Rebuild in current context
514
        return selectedTab;
515
      }
516
517
      /**
518
       * Update the band summary line in the manage area
519
       */
520
      function updateBandSummary() {
521
        var bandName = $('.editTicketTitle').val();
522
        var members = [];
523
        for (var instrument in currentBand) {
524
          if (currentBand.hasOwnProperty(instrument)) {
525
            for (var i = 0; i < currentBand[instrument].length; i++) {
526
              members.push(currentBand[instrument][i]);
527
            }
528
          }
529
        }
530
        var memberList = members.join(', ');
531
        var summaryHtml = (bandName ? bandName + '<br />' : '') + memberList;
532
        $('.selectedBand').html(summaryHtml);
533
      }
534
535
      /**
536
       * Update all instrument tabs with either performer names or 'needed' note
537
       */
538
      function updateInstrumentTabPerformers() {
539
        var performersSpan;
540
        var performerString;
541
542
        for (var iIdx = 0; iIdx < that.instrumentOrder.length; iIdx++) {
543
          var instrument = that.instrumentOrder[iIdx];
544
545
          performersSpan = controlPanelOuter
546
            .find('.instrument[data-instrument-shortcode=' + instrument + ']')
547
            .find('.instrumentPerformer');
548
549
          performerString = currentBand[instrument].join(', ');
550
          if (!performerString) {
551
            performerString = '<i>Needed</i>';
552
          }
553
          performersSpan.html(performerString);
554
        }
555
        updateBandSummary();
556
      }
557
558
      /**
559
       * Handle performer add / remove by performer button / text input
560
       * @param instrument
561
       * @param changedPerformer
562
       * @param isAdd
563
       */
564
      function alterInstrumentPerformerList(instrument, changedPerformer, isAdd) {
565
        var currentInstrumentPerformers = currentBand[selectedInstrument];
566
567
        var newInstrumentPerformers = [];
568
        for (var i = 0; i < currentInstrumentPerformers.length; i++) {
569
          var member = currentInstrumentPerformers[i].trim(); // Trim only required when we draw data from manual input
570
          if (member.length) {
571
            if (member.toUpperCase() != changedPerformer.toUpperCase()) {
572
              // If it's not the name on our button, no change
573
              newInstrumentPerformers.push(member);
574
            }
575
          }
576
        }
577
578
        if (isAdd) { // If we've just selected a new user, append them
579
          newInstrumentPerformers.push(changedPerformer);
580
          if (!that.performerExists(changedPerformer)) {
581
            that.addPerformerByName(changedPerformer);
582
          }
583
        }
584
585
        currentBand[selectedInstrument] = newInstrumentPerformers;
586
        // Now update band with new performers of this instrument
587
588
        updateInstrumentTabPerformers();
589
        rebuildPerformerList();
590
591
        if (newInstrumentPerformers.length) { // If we've a performer for this instrument, skip to next
592
          nextInstrumentTab();
593
        }
594
595
      }
596
597
      /**
598
       * Handle click on a song in manage page search results
599
       *
600
       * @param {{id, title, artist, instruments}} song
601
       */
602
      function applyNewSong(song) {
603
        var selectedId = song.id;
604
        var selectedSong = song.artist + ': ' + song.title;
605
606
        var removeSongButton = $('.removeSongButton');
607
        removeSongButton.removeClass('hidden');
608
609
        // Perform actions with selected song
610
        var editTicketBlock = $('.editTicket'); // Temp hack, should already be in scope?
611
612
        editTicketBlock.find('input.selectedSongId').val(selectedId);
613
        editTicketBlock.find('.selectedSong').text(selectedSong);
614
615
        // Redraw instrument tabs according to current songs
616
        var instrumentDiv = controlPanelOuter.find('.instruments');
617
        instrumentDiv.html(that.manageInstrumentTabsTemplate(song.instruments));
618
        instrumentDiv.find('.instrument').removeClass('instrumentSelected');
619
        instrumentDiv.find('.instrument:first').addClass('instrumentSelected');
620
621
        // Enable the instrument tabs
622
        // var allInstrumentTabs = controlPanelOuter.find('.instrument');
623
624
        instrumentDiv.find('.instrument').click(
625
          function() {
626
            selectedInstrument = $(this).data('instrumentShortcode');
627
            setActiveTab(selectedInstrument);
628
          }
629
        );
630
631
        // Iterate through currentBand and remove any instruments not present in song abbreviations
632
        var validInstruments = song.instruments.map(function(i) {
633
          return i.abbreviation;
634
        });
635
        for (var instrument in currentBand) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
636
          if (validInstruments.indexOf(instrument) === -1) {
637
            currentBand[instrument] = [];
638
          }
639
        }
640
641
        updateInstrumentTabPerformers();
642
        rebuildPerformerList();
643
      }
644
645
      function removeSong() {
646
        var editTicketBlock = $('.editTicket'); // Temp hack, should already be in scope?
647
        editTicketBlock.find('input.selectedSongId').val(0);
648
        editTicketBlock.find('.selectedSong').text('');
649
        $(songSearchInput).val('');
650
        var removeSongButton = $('.removeSongButton');
651
        removeSongButton.hide();
652
653
      }
654
    },
655
656
    manage: function(tickets) {
657
      var that = this;
658
      this.appMessageTarget = $('#appMessages');
659
      this.initTemplates();
660
      var ticket, ticketBlock; // For loop iterations
661
662
      var out = '';
663
      for (var i = 0; i < tickets.length; i++) {
664
        ticket = tickets[i];
665
        out += that.drawManageableTicket(ticket);
666
        ticketBlock = $('.ticket[data-ticket-id="' + ticket.id + '"]');
667
        ticketBlock.data('ticket', ticket);
668
      }
669
      $('#target').html(out);
670
671
      // Find new tickets (now they're DOM'd) and add data to them
672
      for (i = 0; i < tickets.length; i++) {
673
        ticket = tickets[i];
674
        ticketBlock = $('.ticket[data-ticket-id="' + ticket.id + '"]');
675
        ticketBlock.data('ticket', ticket);
676
      }
677
678
      var $sortContainer = $('.sortContainer');
679
      $sortContainer.sortable({
680
        axis: 'y',
681
        update: function(event, ui) {
682
          void(event);
683
          void(ui);
684
          that.ticketOrderChanged();
685
        }
686
      }).disableSelection().css('cursor', 'move');
687
688
      this.enableButtons($sortContainer);
689
690
      this.updatePerformanceStats();
691
692
      this.resetEditTicketBlock();
693
694
    },
695
696
    initSearchPage: function() {
697
      var that = this;
698
      this.initTemplates();
699
      this.enableSongSearchBox('.searchString', '.songComplete', that.searchPageSongSelectionClick);
700
    },
701
702
    initTemplates: function() {
703
      var that = this;
704
705
      // CommaList = each, with commas joining. Returns value at t as tuple {k,v}
706
      // "The options hash contains a function (options.fn) that behaves like a normal compiled Handlebars template."
707
      // If called without inner template, options.fn is not populated
708
      Handlebars.registerHelper('commalist', function(context, options) {
709
        var retList = [];
710
711
        for (var key in context) {
712
          if (context.hasOwnProperty(key)) {
713
            retList.push(options.fn ? options.fn({k: key, v: context[key]}) : context[key]);
714
          }
715
        }
716
717
        return retList.join(', ');
718
      });
719
720
      Handlebars.registerHelper('instrumentIcon', function(instrumentCode) {
721
        var icon = '<span class="instrumentTextIcon">' + instrumentCode + '</span>';
722
        if (that.displayOptions.hasOwnProperty('iconMapHtml')) {
723
          if (that.displayOptions.iconMapHtml.hasOwnProperty(instrumentCode)) {
724
            icon = that.displayOptions.iconMapHtml[instrumentCode];
725
          }
726
        }
727
        return new Handlebars.SafeString(icon);
728
      });
729
730
      Handlebars.registerHelper('durationToMS', function(duration) {
731
        var seconds = (duration % 60);
732
        if (seconds < 10) {
733
          seconds = '0' + seconds;
734
        }
735
        return Math.floor(duration / 60) + ':' + seconds;
736
      });
737
738
      Handlebars.registerHelper('gameList', function(song) {
739
        return song.platforms.join(', ');
740
      });
741
742
      Handlebars.registerHelper('ifContains', function(haystack, needle, options) {
743
        return (haystack.indexOf(needle) === -1) ? '' : options.fn(this);
744
      });
745
746
      this.manageTemplate = Handlebars.compile(
747
        '<div class="ticket well well-sm {{#if ticket.used}}used{{/if}}' +
748
        ' {{#each ticket.song.platforms }}platform{{ this }} {{/each}}' +
749
        ' {{#if ticket.band.K}}withKeys{{/if}}"' +
750
        ' data-ticket-id="{{ ticket.id }}">' +
751
        '        <div class="pull-right">' +
752
        (function() {
753
          var s = '';
754
          for (var i = 0; i < that.platforms.length; i++) {
755
            var p = that.platforms[i];
756
            s += '<div class="gameMarker gameMarker' + p + '">' +
757
              '{{#ifContains ticket.song.platforms "' + p + '" }}' + p + '{{/ifContains}}</div>';
758
          }
759
          return s;
760
        })() +
761
        '        <button class="btn btn-primary performButton" data-ticket-id="{{ ticket.id }}">Performing</button>' +
762
        '        <button class="btn btn-danger removeButton" data-ticket-id="{{ ticket.id }}">Remove</button>' +
763
        '        <button class="btn editButton" data-ticket-id="{{ ticket.id }}">' +
764
        '<span class="fa fa-edit" title="Edit"></span>' +
765
        '</button>' +
766
        '        </div>' +
767
        '<div class="ticketOrder">' +
768
        '<div class="ticketOrdinal"></div>' +
769
        '<div class="ticketTime"></div>' +
770
        '</div>' +
771
        '<div class="ticketId">' +
772
        '<span class="fa fa-ticket"></span> {{ ticket.id }}</div> ' +
773
        '<div class="ticketMeta">' +
774
        '<div class="blocking">' +
775
        '{{#if ticket.blocking}}<span class="fa fa-hand-stop-o" title="Blocking" />{{/if}}' +
776
        '</div>' +
777
        '<div class="private">' +
778
        '{{#if ticket.private}}<span class="fa fa-eye-slash" title="Private" />{{/if}}' +
779
        '</div>' +
780
        '</div>' +
781
        '<div class="pendingSong">' +
782
        '<span class="fa fa-group"></span> ' +
783
784
        // Display performers with metadata if valid, else just the band title.
785
        /*
786
         '{{#if ticket.performers}}' +
787
         '{{#each ticket.performers}}' +
788
         '<span class="performer performerDoneCount{{songsDone}}" ' +
789
         'data-performer-id="{{performerId}}"> {{performerName}} ' +
790
         ' (<span class="songsDone">{{songsDone}}</span>/<span class="songsTotal">{{songsTotal}}</span>)' +
791
         '</span>' +
792
         '{{/each}}' +
793
         '{{else}}' +
794
         '{{ ticket.title }}' +
795
         '{{/if}}' +
796
         */
797
798
        // Display performers with metadata if valid, else just the band title.
799
        '{{#if ticket.band}}' +
800
        '{{#each ticket.band}} <span class="instrumentTextIcon">{{ @key }}</span>' +
801
        '{{#each this}}' +
802
        '<span class="performer performerDoneCount{{songsDone}}" ' +
803
        'data-performer-id="{{performerId}}" data-performer-name="{{performerName}}"> {{performerName}} ' +
804
        ' (<span class="songsDone">{{songsDone}}</span>/<span class="songsTotal">{{songsTotal}}</span>)' +
805
        '</span>' +
806
        '{{/each}}' +
807
        '{{/each}}' +
808
        '{{/if}}' +
809
810
        '{{#if ticket.title}}' +
811
        '<span class="ticketTitleIcon"><span class="instrumentTextIcon">Title</span> {{ ticket.title }}</span>' +
812
        '{{/if}}' +
813
814
        '{{#if ticket.song}}<br /><span class="fa fa-music"></span> {{ticket.song.artist}}: ' +
815
        '{{ticket.song.title}}' +
816
        ' ({{gameList ticket.song}})' +
817
        '{{/if}}' +
818
        '</div>' +
819
        '</div>'
820
      );
821
822
      this.upcomingTicketTemplate = Handlebars.compile(
823
        '<div class="ticket well ' +
824
        (this.displayOptions.songInPreview ? 'withSong' : 'noSong') +
825
        ' ' +
826
        (this.displayOptions.title ? 'withTitle' : 'noTitle') + // TODO is this used (correctly)?
827
        '" data-ticket-id="{{ ticket.id }}">' +
828
829
        (this.displayOptions.adminQueueHasControls && this.displayOptions.isAdmin ?
830
          '<div class="ticketAdminControls">' +
831
          '<button class="btn btn-sm btn-primary performingButton"' +
832
          ' data-ticket-id="{{ ticket.id }}">Performing</button>' +
833
          '<button class="btn btn-sm btn-danger removeButton" data-ticket-id="{{ ticket.id }}">Remove</button>' +
834
          '</div>'
835
          : '') +
836
837
838
        '<div class="ticketMeta">' +
839
        '<div class="blocking">' +
840
        '{{#if ticket.blocking}}<span class="fa fa-hand-stop-o" title="Blocking" />{{/if}}' +
841
        '</div>' +
842
        '<div class="private">' +
843
        '{{#if ticket.private}}<span class="fa fa-eye-slash" title="Private" />{{/if}}' +
844
        '</div>' +
845
        '</div>' +
846
847
        '  <div class="ticket-inner">' +
848
        '    <p class="text-center band auto-font">{{ticket.title}}</p>' +
849
        '    <p class="performers auto-font" data-fixed-assetwidth="200">' +
850
        '{{#each ticket.band}}' +
851
        '<span class="instrumentTag">{{instrumentIcon @key}}</span>' +
852
        '<span class="instrumentPerformers">{{#commalist this}}{{v.performerName}}{{/commalist}}</span>' +
853
        '{{/each}}' +
854
        '    </p>' +
855
        (this.displayOptions.songInPreview ?
856
          '{{#if ticket.song}}<p class="text-center song auto-font">' +
857
          '{{ticket.song.artist}}: {{ticket.song.title}}' +
858
          ' ({{gameList ticket.song}})' +
859
          '</p>{{/if}}' : '') +
860
        '        </div>' +
861
        '</div>  '
862
      );
863
864
      this.songAutocompleteItemTemplate = Handlebars.compile(
865
        '<div class="acSong" data-song-id="{{ song.id }}">' +
866
        '        <div class="acSong-inner {{#if song.queued}}queued{{/if}}">' +
867
        '        {{song.artist}}: {{song.title}} ({{gameList song}}) ' +
868
        '        </div>' +
869
        '</div>  '
870
      );
871
872
      this.editTicketTemplate = Handlebars.compile(
873
        '<div class="editTicket well">' +
874
        '<div class="pull-right editTicketButtons">' +
875
        '<button class="blockingButton btn btn-warning toggleButton">' +
876
        '<span class="fa fa-hand-stop-o" /> Blocking ' +
877
        ' <input type="checkbox" class="blockingCheckbox" ' +
878
        '  {{#if ticket}}{{# if ticket.blocking }}checked="checked"{{/if}}{{/if}} /></button>' +
879
        '<button class="privacyButton btn btn-warning toggleButton">' +
880
        '<span class="fa fa-eye-slash" /> Private ' +
881
        ' <input type="checkbox" class="privateCheckbox" ' +
882
        '  {{#if ticket}}{{# if ticket.private }}checked="checked"{{/if}}{{/if}} /></button>' +
883
        '<button class="editTicketButton btn btn-success">' +
884
        '<span class="fa fa-save" /> Save</button>' +
885
        '<button class="cancelTicketButton btn">' +
886
        '<span class="fa fa-close" /> Cancel</button>' +
887
        '</div>' +
888
889
        '{{# if ticket}}' +
890
        '<h3 class="editTicketHeader">Edit ticket <span class="fa fa-ticket"></span> {{ticket.id}}</h3>' +
891
        '{{else}}<h3 class="newTicketHeader">Add new ticket</h3>{{/if}}' +
892
893
        '<div class="editTicketInner">' +
894
        '<div class="editTicketSong">' +
895
        '<div class="ticketAspectSummary"><span class="fa fa-music fa-2x" title="Song"></span> ' +
896
        '<input type="hidden" class="selectedSongId"/> ' +
897
        '<span class="selectedSong">{{#if ticket}}{{#if ticket.song}}{{ticket.song.artist}}: ' +
898
        '{{ticket.song.title}}{{/if}}{{/if}}</span>' +
899
900
        '<button title="Remove song from ticket" ' +
901
        'class="btn removeSongButton{{#unless ticket}}{{#unless ticket.song}} hidden{{/unless}}{{/unless}}">' +
902
        ' <span class="fa fa-ban" />' +
903
        '</button>' +
904
905
        '</div>' +
906
        '<div class="input-group input-group">' +
907
        '<span class="input-group-addon" id="search-addon1"><span class="fa fa-search"></span> </span>' +
908
        '<input class="addSongTitle form-control" placeholder="Search song or use code"/>' +
909
        '</div>' +
910
911
        '<div class="songCompleteOuter">' +
912
        '<div class="songComplete"></div>' +
913
        '</div>' + // /songCompleteOuter
914
        '</div>' + // /editTicketSong
915
916
        '<div class="editTicketBandColumn">' +
917
918
        '<div class="ticketAspectSummary"><span class="fa fa-group fa-2x pull-left" title="Performers"></span>' +
919
        '<span class="selectedBand">{{#if ticket}}{{ticket.title}}{{/if}}</span>' +
920
        '</div>' + // /ticketAspectSummary
921
922
        '<div class="input-group">' +
923
        '<span class="input-group-addon" id="group-addon-band"><span class="fa fa-pencil"></span> </span>' +
924
        '<input class="editTicketTitle form-control" placeholder="Band name or message (optional)"' +
925
        ' value="{{#if ticket}}{{ticket.title}}{{/if}}"/>' +
926
        '</div>' + // /input-group
927
928
        '<div class="bandControls">' +
929
        '<div class="bandTabsOuter">' +
930
        '<div class="instruments">' +
931
        '</div>' + // /instruments
932
        '<div class="performerSelect">' +
933
        '<div class="input-group input-group">' +
934
        '<span class="input-group-addon" id="group-addon-performer"><span class="fa fa-plus"></span> </span>' +
935
        '<input class="newPerformer form-control" placeholder="New performer (Firstname Initial)"/>' +
936
        '</div>' +
937
938
        '<div class="performers"></div>' +
939
        '</div>' + // /performerSelect
940
        '</div>' + // /bandTabsOuter
941
        '</div>' + // /bandControls
942
        '</div>' + // /editTicketBandColumn
943
        '<div class="clearfix"></div>' + // Clear after editTicketBandColumn
944
        '</div>' + // /editTicketInner
945
        '</div>' // /editTicket
946
      );
947
948
      this.manageInstrumentTabsTemplate = Handlebars.compile(
949
        '{{#each this}}' +
950
        ' <div class="instrument instrument{{ this.abbreviation }}" ' +
951
        '   data-instrument-shortcode="{{ this.abbreviation }}">' +
952
        '  <div class="instrumentName">{{ this.name }}</div>' +
953
        '  <div class="instrumentPerformer"><i>Needed</i></div>' +
954
        ' </div>' +
955
        '{{/each}}'
956
      );
957
958
      this.songDetailsTemplate = Handlebars.compile(
959
        '<div class="songDetails"><h3>{{song.artist}}: {{song.title}}</h3>' +
960
        '<table>' +
961
        '<tr><th>Duration</th><td>{{durationToMS song.duration}}</td></tr> ' +
962
        '<tr><th>Code</th><td>{{song.codeNumber}}</td></tr> ' +
963
        '<tr><th>Instruments </th><td>{{commalist song.instruments}}</td></tr> ' +
964
        '<tr><th>Games</th><td>{{commalist song.platforms}}</td></tr> ' +
965
        '<tr><th>Source</th><td>{{song.source}}</td></tr> ' +
966
        '</table>' +
967
        '</div>'
968
      );
969
970
    },
971
972
    ticketOrderChanged: function() {
973
      var that = this;
974
      var idOrder = [];
975
      $('#target').find('.ticket').each(
976
        function() {
977
          var ticketBlock = $(this);
978
          var ticketId = ticketBlock.data('ticketId');
979
          idOrder.push(ticketId);
980
        }
981
      );
982
983
      that.showAppMessage('Updating ticket order');
984
      $.ajax({
985
        method: 'POST',
986
        data: {
987
          idOrder: idOrder
988
        },
989
        url: '/api/newOrder',
990
        success: function(data, status) {
0 ignored issues
show
Unused Code introduced by
The parameter status is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
Unused Code introduced by
The parameter data is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
991
          // FIXME check return status
992
          that.showAppMessage('Saved revised order', 'success');
993
        },
994
        error: function(xhr, status, error) {
995
          var message = 'Failed to save revised order';
996
          that.reportAjaxError(message, xhr, status, error);
997
        }
998
      });
999
1000
      this.updatePerformanceStats();
1001
    },
1002
1003
    performButtonCallback: function(button) {
1004
      var that = this;
1005
1006
      button = $(button);
1007
      var ticketId = button.data('ticketId');
1008
      that.showAppMessage('Mark ticket used');
1009
      $.ajax({
1010
          method: 'POST',
1011
          data: {
1012
            ticketId: ticketId
1013
          },
1014
          url: '/api/useTicket',
1015
          success: function(data, status) {
1016
            that.showAppMessage('Marked ticket used', 'success');
1017
            void(data);
1018
            void(status);
1019
            var ticketBlock = $('.ticket[data-ticket-id="' + ticketId + '"]');
1020
            ticketBlock.addClass('used');
1021
            // TicketBlock.append(' (done)');
1022
1023
            // Fixme receive updated ticket info from API
1024
            var ticket = ticketBlock.data('ticket');
1025
            ticket.startTime = Date.now() / 1000;
1026
            ticket.used = true;
1027
            ticketBlock.data('ticket', ticket);
1028
1029
            that.updatePerformanceStats();
1030
          },
1031
          error: function(xhr, status, error) {
1032
            var message = 'Failed to mark ticket used';
1033
            that.reportAjaxError(message, xhr, status, error);
1034
          }
1035
        }
1036
      );
1037
    },
1038
1039
    removeButtonCallback: function(button) {
1040
      var that = this;
1041
      button = $(button);
1042
      var ticketId = button.data('ticketId');
1043
      that.showAppMessage('Deleting ticket');
1044
      $.ajax({
1045
          method: 'POST',
1046
          data: {
1047
            ticketId: ticketId
1048
          },
1049
          url: '/api/deleteTicket',
1050
          success: function(data, status) {
1051
            that.showAppMessage('Deleted ticket', 'success');
1052
            void(status);
1053
            var ticketBlock = $('.ticket[data-ticket-id="' + ticketId + '"]');
1054
            ticketBlock.remove();
1055
            that.updatePerformanceStats();
1056
          },
1057
          error: function(xhr, status, error) {
1058
            var message = 'Failed to deleted ticket';
1059
            that.reportAjaxError(message, xhr, status, error);
1060
          }
1061
        }
1062
      );
1063
    },
1064
1065
    editButtonCallback: function(button) {
1066
      var that = this;
1067
      button = $(button);
1068
      var ticketId = button.data('ticketId');
1069
1070
      var ticketBlock = $('.ticket[data-ticket-id="' + ticketId + '"]');
1071
      var ticket = ticketBlock.data('ticket'); // TODO possibly load from ajax instead?
1072
      that.resetEditTicketBlock(ticket);
1073
    },
1074
1075
    enableAcSongSelector: function(outerElement, songClickHandler) {
1076
      var that = this;
1077
      outerElement.find('.acSong').click(
1078
        function() {
1079
          // Find & decorate clicked element
1080
          outerElement.find('.acSong').removeClass('selected');
1081
          $(this).addClass('selected');
1082
1083
          var song = $(this).data('song');
1084
          songClickHandler.call(that, song); // Run in 'that' context
1085
        }
1086
      );
1087
    },
1088
1089
1090
    searchPageSongSelectionClick: function(song) {
1091
      var target = $('#searchTarget');
1092
      song.instruments = song.instruments.map(function(s) {
1093
        return s.name;
1094
      }); // Unwrap objects
1095
      target.html(this.songDetailsTemplate({song: song}));
1096
    },
1097
1098
    updatePerformanceStats: function() {
1099
      var that = this;
1100
      var performed = {};
1101
      var lastByPerformer = {};
1102
      var ticketOrdinal = 1;
1103
      var ticketTime = null;
1104
1105
      var pad = function(number) {
1106
        if (number < 10) {
1107
          return '0' + number;
1108
        }
1109
        return number;
1110
      };
1111
1112
      // First check number of songs performed before this one
1113
      var sortContainer = $('.sortContainer');
1114
      var lastSongDuration = null;
1115
      var lastTicketNoSong = true;
1116
1117
      var nthUnused = 1;
1118
1119
      sortContainer.find('.ticket').each(function() {
1120
        var realTime;
1121
        var ticketId = $(this).data('ticket-id');
1122
        var ticketData = $(this).data('ticket');
1123
1124
        if (ticketData.startTime) {
1125
          realTime = new Date(ticketData.startTime * 1000);
1126
        }
1127
1128
        $(this).removeClass('shown');
1129
1130
        if (!(ticketData.used || ticketData.private)) {
1131
          if (nthUnused <= that.displayOptions.upcomingCount) {
1132
            $(this).addClass('shown');
1133
          }
1134
          nthUnused++;
1135
        }
1136
1137
        $(this).find('.ticketOrdinal').text('# ' + ticketOrdinal);
1138
        // Fixme read ticketStart from data if present
1139
        if (realTime) {
1140
          ticketTime = realTime;
1141
        } else if (ticketTime) {
1142
          // If last song had an implicit time, add defaultSongOffsetMs to it and assume next song starts then
1143
          // If this is in the past, assume it starts now!
1144
          var songOffsetMs;
1145
          if (lastTicketNoSong) {
1146
            songOffsetMs = that.defaultSongIntervalSeconds * 1000;
1147
            // Could just be a message, could be a reset / announcement, so treat as an interval only
1148
          } else if (lastSongDuration) {
1149
            songOffsetMs = (that.defaultSongIntervalSeconds + lastSongDuration) * 1000;
1150
          } else {
1151
            songOffsetMs = (that.defaultSongIntervalSeconds + that.defaultSongLengthSeconds) * 1000;
1152
          }
1153
          ticketTime = new Date(Math.max(ticketTime.getTime() + songOffsetMs, Date.now()));
1154
        } else {
1155
          ticketTime = new Date();
1156
        }
1157
        $(this).find('.ticketTime').text(pad(ticketTime.getHours()) + ':' + pad(ticketTime.getMinutes()));
1158
1159
        // Update performer stats (done/total)
1160
        $(this).find('.performer').each(function() {
1161
          var performerId = $(this).data('performer-id');
1162
          var performerName = $(this).data('performer-name');
1163
          if (!performed.hasOwnProperty(performerId)) {
1164
            performed[performerId] = 0;
1165
          }
1166
          $(this).find('.songsDone').text(performed[performerId]);
1167
1168
          $(this).removeClass(
1169
            function(i, oldClass) {
1170
              void(i);
1171
              var classes = oldClass.split(' ');
1172
              var toRemove = [];
1173
              for (var cIdx = 0; cIdx < classes.length; cIdx++) {
1174
                if (classes[cIdx].match(/^performerDoneCount/)) {
1175
                  toRemove.push(classes[cIdx]);
1176
                }
1177
              }
1178
              return toRemove.join(' ');
1179
            }
1180
          ).addClass('performerDoneCount' + performed[performerId]);
1181
          performed[performerId]++;
1182
1183
          // Now check proximity of last song by this performer
1184
          if (lastByPerformer.hasOwnProperty(performerId)) {
1185
            var distance = ticketOrdinal - lastByPerformer[performerId].idx;
1186
            $(this).removeClass('proximityIssue');
1187
            $(this).removeClass('proximityIssue1');
1188
            if ((distance < 3) && (performerName.charAt(0) !== '?')) {
1189
              $(this).addClass('proximityIssue');
1190
              if (distance === 1) {
1191
                $(this).addClass('proximityIssue1');
1192
              }
1193
            }
1194
          } else {
1195
            // Make sure they've not got a proximity marker on a ticket that's been dragged to top
1196
            $(this).removeClass('proximityIssue');
1197
          }
1198
          lastByPerformer[performerId] = {idx: ticketOrdinal, ticketId: ticketId};
1199
        });
1200
        ticketOrdinal++;
1201
1202
        if (ticketData.song) {
1203
          lastSongDuration = ticketData.song.duration;
1204
          lastTicketNoSong = false;
1205
        } else {
1206
          lastSongDuration = 0;
1207
          lastTicketNoSong = true;
1208
        } // Set non-song ticket to minimum duration
1209
      });
1210
1211
      // Then update all totals
1212
      sortContainer.find('.performer').each(function() {
1213
        var performerId = $(this).data('performer-id');
1214
        var totalPerformed = performed[performerId];
1215
        $(this).find('.songsTotal').text(totalPerformed);
1216
      });
1217
    },
1218
1219
    /**
1220
     * Show a message in the defined appMessageTarget (f any)
1221
     *
1222
     * @param message {string} Message to show (replaces any other)
1223
     * @param [className='info'] {string} 'info','success','warning','danger'
0 ignored issues
show
Documentation Bug introduced by
The parameter className='info' does not exist. Did you maybe mean className instead?
Loading history...
1224
     */
1225
    showAppMessage: function(message, className) {
1226
      var that = this;
1227
      if (this.messageTimer) {
1228
        clearTimeout(this.messageTimer);
1229
      }
1230
1231
      this.messageTimer = setTimeout(function() {
1232
        that.appMessageTarget.html('');
1233
      }, 5000);
1234
1235
      if (!className) {
1236
        className = 'info';
1237
      }
1238
      if (this.appMessageTarget) {
1239
        var block = $('<div />').addClass('alert alert-' + className);
1240
        block.text(message);
1241
        this.appMessageTarget.html('').append(block);
1242
      }
1243
    },
1244
1245
    ucFirst: function(string) {
1246
      return string.charAt(0).toUpperCase() + string.slice(1);
1247
    },
1248
1249
    reportAjaxError: function(message, xhr, status, error) {
1250
      this.showAppMessage(
1251
        this.ucFirst(status) + ': ' + message + ': ' + error + ', ' + xhr.responseJSON.error,
1252
        'danger'
1253
      );
1254
    },
1255
1256
    checkRemoteRedirect: function() {
1257
      window.setInterval(function() {
1258
          $.get('/api/remotesRedirect', function(newPath) {
1259
            if (newPath && (newPath !== window.location.pathname)) {
1260
              window.location.pathname = newPath;
1261
            }
1262
          });
1263
        },
1264
        10000);
1265
    }
1266
  };
1267
}());